module pcc.compiler;

public class Compiler {
    private string[] files;
    private string[] cflags;
    private string output_directory;

    public this() {
        // TODO: check if DUB provides an environment variable
        output_directory = ".";
    }

    /**
     * Configures the output directory where all object files and
     * static libraries will be located.
     */
    public void setOutputDirectory(string outputDirectory) {
        if (outputDirectory == "") {
            return;
        }
        this.output_directory = outputDirectory;
    }

    /// Add a file to be compiled.
    public void addFile(string filename) {
        this.files ~= filename;
    }

    /// Add a directory to be include path (`-I`) for headers.
    public void addIncludeDirectory(string directory) {
        this.cflags ~= "-I" ~ directory;
    }

    /**
     * Runs the compiler.
     */
    public void compileTo(string target) {
        import std.path : buildPath;
        import std.string;

        string lib_name;
        string gnu_lib_name;

        if (target.startsWith("lib") && target.endsWith(".a")) {
            lib_name = target[3..$ - 2];
            gnu_lib_name = target;
        } else {
            lib_name = target;
            gnu_lib_name = "lib" ~ target ~ ".a";
        }

        Object[] object_files = generate_objects_from_files(this.files, this.output_directory);
        debug assert(object_files.length == this.files.length);

        this.compile_objects(object_files);
        this.assemble(buildPath(this.output_directory, gnu_lib_name), object_files);
    }

    private void compile_objects(const ref Object[] objects) {
        import std.parallelism : parallel;
        import std.process : execute;

        foreach(object; objects.parallel) {
            const cmd = this.create_object_compile_cmd(object);
            execute(cmd);
        }
    }

    private string[] create_object_compile_cmd(const ref Object object) {
        string[] args = ["cc"];
        foreach(flag; this.cflags) {
            args ~= flag;
        }
        args ~= "-c";
        args ~= object.source;

        args ~= "-o";
        args ~= object.dest;
        return args;
    }

    private void assemble(string dest, const ref Object[] objects) {
        import std.algorithm.iteration : map;
        import std.file : remove;
        import std.range : chunks;
        import std.process : execute;

        // Delete the destination if it exists as we want to
        // create on the first iteration instead of appending.
        try {
            remove(dest);
        } catch (Exception e) {}

        auto objs = objects.map!((object) => object.dest);
        foreach(ref chunk; chunks(objs, 100)) {
            foreach(c; chunk) {
                execute(["ar", "cq", dest] ~ c);
            }
        }
        execute(["ar", "s", dest]);
    }
}

private struct Object {
    string source;
    string dest;
}

private Object[] generate_objects_from_files(const ref string[] files, string target) {
    import std.format : format;
    import std.file : mkdirRecurse;
    import std.path : baseName, dirName, setExtension;

    Object[] objs;
    objs.reserve(files.length);
    foreach(const ref file; files) {
        const base = baseName(file);
        string dir = baseName(dirName(file));
        const obj_hash = format("%010x-%s", dir.toHash(), base).setExtension("o");
        mkdirRecurse(dirName(obj_hash));
        objs ~= Object(file, obj_hash);
    }
    return objs;
}

private size_t toHash(inout const string self) @safe @nogc nothrow {
    import std.math : pow;

    // borrowed from Java: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#hashCode--
    int hash = 0;
    for (int i = 0; i < self.length; i++) {
        hash += self[i] * pow(31, cast(int)self.length - 1 - i);
    }
    return hash;
}

unittest
{
    import std.format;

    assert(0 == "".toHash(), "empty string hash is not 0");
    assert(69609650 == "Hello".toHash(),
        "\"Hello\".toHash():\nExpected: 69609650\nActual  : %d".format("Hello".toHash()));
    assert(1794106052 == "hello world".toHash(),
        "\"hello world\".toHash():\nExpected: 1794106052\nActual  : %d".format("hello world".toHash()));
}

